import tkinter as tk
from tkinter import messagebox, ttk, Frame, Menu
import re
import sqlite3
from PIL import Image, ImageDraw
import pickle
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
import tkinter
from tkinter import *
from tkinter import ttk, Frame, Tk, messagebox, Menu
from PIL import Image, ImageDraw
import csv
from sklearn.datasets import load_digits
from sklearn.preprocessing import LabelBinarizer

success = 'user_data.db'

MAX_USERNAME_LENGTH = 20  # 最大用户名长度限制
MAX_PASSWORD_LENGTH = 20  # 最大密码长度限制
MIN_USERNAME_LENGTH = 6  # 最小用户名长度限制
MIN_PASSWORD_LENGTH = 6  # 最小密码长度限制

# 数据库操作函数
def create_users_table(conn):
    cursor = conn.cursor()
    cursor.execute('''CREATE TABLE IF NOT EXISTS users
                    (id INTEGER PRIMARY KEY AUTOINCREMENT,
                    username TEXT NOT NULL,
                    password TEXT NOT NULL)''')
    conn.commit()

def read_users_from_db(conn):
    users = {}
    try:
        cursor = conn.cursor()
        cursor.execute('SELECT username, password FROM users')
        rows = cursor.fetchall()
        for row in rows:
            users[row[0]] = row[1]
    except Exception as e:
        print(f"Error reading users from DB: {e}")
    return users

def add_user_to_db(conn, username, password):
    try:
        cursor = conn.cursor()
        cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", (username, password))
        conn.commit()
        print("User added to DB successfully.")
    except Exception as e:
        print(f"Error adding user to DB: {e}")

def is_valid_input(input_str):
    # 使用正则表达式检查输入是否只包含数字和字母
    return bool(re.match("^[a-zA-Z0-9]+$", input_str))

# 登录函数
def sign_in_f():
    user_si_name = nameval.get()
    user_si_pass = passwordval.get()

    conn = sqlite3.connect(success)
    users = read_users_from_db(conn)

    if user_si_name in users and users[user_si_name] == user_si_pass:
        messagebox.showinfo("登录", "登录成功")
        window.destroy()
        open_handwriting_recognition_app()
    else:
        messagebox.showerror("登录错误", "用户名或密码无效")

# 注册函数
def sign_up_f():
    def su_conf_b():
        su_username = su_name_val.get()
        su_userpass = su_pass_val.get()
        su_usercpass = su_cpass_val.get()

        conn = sqlite3.connect(success)

        # 检测用户名和密码是否包含非法字符
        if not is_valid_input(su_username) or not is_valid_input(su_userpass):
            messagebox.showerror("注册错误", "用户名或密码只能包含数字和字母，请重试。")
            return

        # 检查用户名和密码长度是否符合要求
        if len(su_username) > MAX_USERNAME_LENGTH or len(su_userpass) > MAX_PASSWORD_LENGTH:
            messagebox.showerror("注册错误", f"用户名和密码长度不能超过{MAX_USERNAME_LENGTH}个字符。")
            return

        if len(su_username) < MIN_USERNAME_LENGTH or len(su_userpass) < MIN_PASSWORD_LENGTH:
            messagebox.showerror("注册错误", f"用户名和密码长度不能低于{MIN_USERNAME_LENGTH}个字符。")
            return

        if su_userpass == su_usercpass:
            add_user_to_db(conn, su_username, su_userpass)
            messagebox.showinfo("注册", "注册成功，请登录。")
            singn_up_w.destroy()
        else:
            messagebox.showerror("注册错误", "密码不匹配，请重试。")

    singn_up_w = tk.Toplevel()
    singn_up_w.title("用户注册")
    singn_up_w.geometry("400x300")

    su_name_lable = tk.Label(singn_up_w, text="用户名:", font=(12))
    su_pass_lable = tk.Label(singn_up_w, text="密码:", font=(12))
    su_cpass_lable = tk.Label(singn_up_w, text="确认密码:", font=(12))
    su_name_lable.place(x=60, y=80)
    su_pass_lable.place(x=60, y=120)
    su_cpass_lable.place(x=60, y=160)

    su_name_val = tk.StringVar()
    su_pass_val = tk.StringVar()
    su_cpass_val = tk.StringVar()
    su_name_entry = tk.Entry(singn_up_w, textvariable=su_name_val, width=20, font=(12))
    su_pass_entry = tk.Entry(singn_up_w, textvariable=su_pass_val, width=20, show="*", font=(12))
    su_cpass_entry = tk.Entry(singn_up_w, textvariable=su_cpass_val, width=20, show="*", font=(12))
    su_name_entry.place(x=180, y=80)
    su_pass_entry.place(x=180, y=120)
    su_cpass_entry.place(x=180, y=160)

    su_confirm_button = tk.Button(singn_up_w, text="注册", command=su_conf_b)
    su_confirm_button.place(x=120, y=200)

    # 添加取消按钮
    su_cancel_button = tk.Button(singn_up_w, text="取消", command=singn_up_w.destroy)
    su_cancel_button.place(x=220, y=200)

def sigmoid(x):
    return 1/(1+np.exp(-x))

def dsigmoid(x):
    return x*(1-x)

class NeuralNetwork:
    def __init__(self,layers):  # (64,100,50,10)
        # 权值的初始化，范围-1到1
        self.U = np.random.random((layers[0]+1,layers[1]+1))*2-1
        self.V = np.random.random((layers[1]+1,layers[2]+1))*2-1
        self.W = np.random.random((layers[2]+1,layers[3]))*2-1
        
    def train(self,X,y,X_test,y_test,lr=0.11,epochs=10000):
        # 添加偏置
        temp = np.ones([X.shape[0],X.shape[1]+1])
        temp[:,0:-1] = X  # 最后一列都是1
        X = temp
        
        for n in range(epochs+1):
            i = np.random.randint(X.shape[0]) # 随机选取一个数据
            x = [X[i]]
            x = np.atleast_2d(x)  # 转为2维数据 (1, 65)

            L0 = sigmoid(np.dot(x,self.U))
            L1 = sigmoid(np.dot(L0,self.V))  # 隐层输出
            L2 = sigmoid(np.dot(L1,self.W))  # 输出层输出
            
            L2_delta = (y[i]-L2)*dsigmoid(L2)
            L1_delta= L2_delta.dot(self.W.T)*dsigmoid(L1)
            L0_delta= L1_delta.dot(self.V.T)*dsigmoid(L0)
            
            self.W += lr*L1.T.dot(L2_delta)
            self.V += lr*L0.T.dot(L1_delta)
            self.U += lr*x.T.dot(L0_delta)
            
            #每训练1000次预测一次准确率
            if n%1000==0:
                predictions = []
                for j in range(X_test.shape[0]):
                    o = self.predict(X_test[j])
                    predictions.append(np.argmax(o)) # 获取预测结果
                self.accuracy = np.mean(np.equal(predictions,y_test))
                print('epoch:',n,'accuracy:',self.accuracy)
        
    def predict(self,x):
        #添加偏置
        temp = np.ones(x.shape[0]+1)
        temp[0:-1] = x
        x = temp
        x = np.atleast_2d(x) # 转为2维数据
        
        L0 = sigmoid(np.dot(x,self.U))
        L1 = sigmoid(np.dot(L0,self.V))  # 隐层输出
        L2 = sigmoid(np.dot(L1,self.W))  # 输出层输出
        
        return L2

# 打开手写数字识别系统
def open_handwriting_recognition_app():
    handwriting_app = tk.Tk()
    handwriting_app.title('手写数字识别demo')
    handwriting_app.geometry('500x300')

    class HandwritingApp(tk.Frame):
        def __init__(self, parent):
            super().__init__(parent)
            self.parent = parent
            self.init_window()
        
        def init_window(self):
            self.parent.title('手写数字识别demo')
            menubar = Menu(self.parent)
            self.parent.config(menu=menubar)
            
            menu = Menu(menubar)
            menu.add_command(label="训练新的模型", command=self.retrainning)
            menu.add_command(label="训练新的模型（含新样本）", command=self.train_with_newsample)
            menu.add_command(label="关于", command=self.aboutme)
            menubar.add_cascade(label="菜单", menu=menu)

            self.frame_info = ttk.LabelFrame(self.parent, text='Info: ' )
            self.frame_info.place(x=15,y=0)
            self.infoLabel = ttk.Label(self.frame_info, text="写字→载入模型→数字识别",anchor="center",font=("微软雅黑",9))
            self.infoLabel.pack(fill=tk.BOTH, expand=tk.YES)
            
            self.frame_pad = ttk.LabelFrame(self.parent, text="写字区")
            self.frame_pad.place(x=10, y=50, width=200, height=200)
            
            # 创建画布
            image = tk.PhotoImage()
            self.canvas = tk.Canvas(self.frame_pad, bg='white', width=200, height=200)
            self.canvas.create_image(120, 120, image=image)
            self.canvas.bind('<B1-Motion>', self.onLeftButtonMove)
            self.canvas.bind('<Button-1>', self.onLeftButtonDown)
            self.canvas.bind('<ButtonRelease-1>', self.onLeftButtonUp)
            self.canvas.pack(fill=tk.BOTH, expand=tk.YES)
            self.base = Image.new("RGB", (200, 200), (255,255,255))
            self.d = ImageDraw.Draw(self.base)
            
            self.X = tk.IntVar()
            self.Y = tk.IntVar()
            self.yesno = tk.IntVar()
            
            action_frame = ttk.Frame(self.parent)
            action_frame.place(x=225,y=65,width=70,height=150)
            button_cl = ttk.Button(action_frame, text="重写", command=self.Clear)        
            button_cl.pack(pady=5)
            button_start = ttk.Button(action_frame, text="载入模型", command=self.load_model)
            button_start.pack(pady=15)
            button_reg = ttk.Button(action_frame, text="数字识别", command=self.predict)
            button_reg.pack(pady=5)  
        
            self.frame2 = ttk.LabelFrame(self.parent, text="数字识别结果")
            self.frame2.place(x=320, y=50, width=150, height=150)
            image2 = tk.PhotoImage()
            self.canvas2 = tk.Canvas(self.frame2, bg='white', width=200, height=200)
            self.canvas2.create_image(120, 120, image=image2)
            self.canvas2.pack(fill=tk.BOTH, expand=tk.YES)

        # 按住鼠标左键移动，画图
        def onLeftButtonMove(self,event):
            self.canvas.create_oval(self.X.get(), self.Y.get(), event.x, event.y, width=8, fill='black')
            self.d.line([self.X.get(), self.Y.get(), event.x, event.y], width=8, fill='black')
            self.X.set(event.x)
            self.Y.set(event.y)
            
        # 鼠标左键单击，允许画图
        def onLeftButtonDown(self,event):
            self.X.set(event.x)
            self.Y.set(event.y)
            self.canvas.create_oval(event.x, event.y, event.x, event.y, width=8, fill='black')
            self.d.line([event.x, event.y, event.x, event.y], width=8, fill='black')

        # 鼠标左键抬起，禁止画图
        def onLeftButtonUp(self,event):
            self.yesno.set(0)

        def Clear(self):
            self.canvas.delete("all")
            self.base = Image.new("RGB", (200, 200), (255,255,255))
            self.d = ImageDraw.Draw(self.base)
            
        def trainning(self,newSample=None):
            digits = load_digits()  # 载入数据
            X = digits.data  # 数据
            y = digits.target  # 标签
            # 输入数据归一化
            X -= X.min()
            X /= X.max()

            if newSample is not None:
                X = np.concatenate((X, newSample[:,:-1]))
                y = np.concatenate((y, newSample[:,-1].astype(int)))
            
            nm = NeuralNetwork([64,100,50,10]) # 创建网络

            X_train,X_test,y_train,y_test = train_test_split(X,y)
            # labels_train = LabelBinarizer().fit_transform(y_train)
            # ArithmeticError labels_test = LabelBinarizer().fit_transform(y_test)
            # 用numpy进行onehot得到的是浮点数，须要修改一下数据格式。
            labels_train = np.eye(10)[y_train].astype(np.int16)
            labels_test = np.eye(10)[y_test].astype(np.int16)

            print('start')
            nm.train(X_train,labels_train,X_test,y_test,epochs=20000)
            print('end')
        
            return nm


        def train_with_newsample(self):
            try:
                new_samples = np.genfromtxt('mysamples.csv', delimiter=',')

                if len(new_samples) > 5:
                    self.model = self.trainning(newSample=new_samples)
                else:
                    self.infoLabel['text'] = '提示：mysamples.csv文件没有足够的新样本'
                    self.infoLabel['foreground'] = ['red']
            except:
                self.infoLabel['text'] = '提示：当前目录未找到mysamples.csv文件，或数据格式有误！'
                self.infoLabel['foreground'] = ['red']

        
        def retrainning(self):
            print('retrainning...')
            self.infoLabel['text'] = '提示：正在训练新的模型。。。'
            self.infoLabel['foreground'] = ['blue']
            self.model = self.trainning()

            with open('nmModel.pkl', 'wb') as pkl:
                pickle.dump(self.model, pkl, pickle.HIGHEST_PROTOCOL)
                self.infoLabel['text'] = '提示：新模型训练完成！'
                self.infoLabel['foreground'] = ['blue']

        
        def load_model(self):
            try:
                pkl = open('nmModel.pkl', 'rb')
                self.model = pickle.load(pkl)

            except:
                self.infoLabel['text'] = '提示：未找到本地模型，正在训练新的模型。'
                self.infoLabel['foreground'] = ['red']
                self.model = self.trainning()

                with open('nmModel.pkl', 'wb') as pkl:
                    pickle.dump(self.model, pkl, pickle.HIGHEST_PROTOCOL)
            finally:
                self.infoLabel['text'] = '提示：模型加载完成'
                self.infoLabel['foreground'] = ['blue']


        def predict(self):
        
            preproces = self.pre_job()
            if not preproces:
                return

            for item in self.canvas2.find_all():
                self.canvas2.delete(item)

            try:
                result = self.model.predict(self.test)
            except AttributeError:
                self.canvas2.create_text(18, 65,
                text = '模型未加载\n或加载失败\n请重载模型',
                font = ("微软雅黑", 16, "bold"),
                fill= "red",
                anchor = W,
                justify = LEFT)

                return

            print(np.argmax(result))

            titleFont = ("微软雅黑", 50, "bold")
            self.canvas2.create_text(45, 65,
                text = np.argmax(result),
                font = titleFont,
                fill= "Turquoise",
                anchor = W,
                justify = LEFT)
            
        def pre_job(self):
            img = self.base
            x,y = img.size
            img = img.convert('L')
            raw_data = img.load()

            "这里有点奇怪，横纵颠倒了？"
            "行列和横纵坐标"
            L = [[raw_data[j, i] for j in range(img.size[0])] for i in range(img.size[1])]
            L_arry = np.array(L)
            print(L_arry.shape)

            row_member = L_arry.sum(axis=1) < 245*img.size[0]
            col_member = L_arry.sum(axis=0) < 245*img.size[1]

            # 图片裁剪的边缘
            r_cs = row_member.cumsum()

            if r_cs.max() < 2:
                # 过滤少于2行非白色像素，即没有画数字的情况
                self.canvas2.create_text(15, 38,
                text = '请先在写字\n区写数字',
                font = ("微软雅黑", 18, "bold"),
                fill= "red",
                anchor = W,
                justify = LEFT)

                return
            y_min =np.argwhere(r_cs == 1)[0,0] - 1 # 第一个非纯白的列
            y_max = r_cs.argmax() + 1

            c_cs = col_member.cumsum()
            x_min = np.argwhere(c_cs == 1)[0,0] - 1
            x_max = c_cs.argmax() + 1

            # 要裁剪成矩形，需要检查一下横竖边
            x_len = x_max - x_min
            y_len = y_max - y_min
            if y_len - x_len > 0:
                x_min = x_min - int(1/2 * (y_len - x_len))
                if x_min < 0:
                    x_min = 0
                x_max = x_min + y_len
            elif y_len - x_len < 0:
                y_min = y_min - int(1/2 * (x_len - y_len))
                if y_min < 0:
                    y_min = 0
                y_max = y_min + x_len

            new = img.crop((x_min, y_min, x_max, y_max))
            print(new.size)

            new = new.resize((8, 8))  # 裁剪成和训练数据一样的尺寸
            new_data = new.load()
            new_array = np.array([[new_data[j, i] for j in range(8)] for i in range(8)])
            print(new_array.shape)

            test = (255 - new_array) / 255

            self.test = np.r_[test.ravel()]

            print(self.test)

            return 'Done'
        def aboutme(self):
            messagebox.showinfo("关于","\n\n手写数字识别demo\n\n")
    app = HandwritingApp(handwriting_app)
    app.pack(fill="both", expand=True)
    handwriting_app.mainloop()
        
# 创建主窗口
window = tk.Tk()
window.geometry("400x300")
window.title("登录系统")

# 创建标签和输入框
name_label = tk.Label(window, text="用户名:")
password_label = tk.Label(window, text="密码:")
name_label.place(x=60, y=80)
password_label.place(x=60, y=120)

nameval = tk.StringVar()
passwordval = tk.StringVar()
name_entry = tk.Entry(window, textvariable=nameval, width=20)
password_entry = tk.Entry(window, textvariable=passwordval, width=20, show="*")
name_entry.place(x=160, y=80)
password_entry.place(x=160, y=120)

# 创建登录和注册按钮
signin_button = tk.Button(window, text="登录", command=sign_in_f)
signup_button = tk.Button(window, text="注册", command=sign_up_f)
signin_button.place(x=120, y=180)
signup_button.place(x=220, y=180)

conn = sqlite3.connect(success)
create_users_table(conn)
# 运行主窗口
window.mainloop()
